﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
// Krzysztof Heim
// pktools.codeplex.com

namespace PKTools.Database.Generator
{
    abstract class Generator
    {
        public class ObjectBase
        {
            public int
                ID;
            public string
                Code;
        }

        public sealed class Table : ObjectBase
        {
            public bool
                IsStamped,
                IsTable;
            public List<Column>
                Columns = new List<Column>();

            public bool HasPK()
            {
                return this.Columns.Find(e => e.IsPK) != null;
            }
        }

        public sealed class Column : ObjectBase
        {
            public Type
                CSharpType;
            public bool
                IsPK,
                IsNull,
                IsIdentity;

            public string GetCSharpType()
            {
                return ToCSharpType(this.CSharpType, this.IsNull);
            }

            public static string ToCSharpType(Type type, bool isNullable)
            {
                return type.Name + (isNullable && type.IsValueType ? "?" : string.Empty);
            }
        }

        protected List<Table>
            tables;
        protected string
            stampColumn;
        protected Sql
            sql;
        protected readonly HashSet<string>
            excludeColumns;

        protected abstract Type GetCSharpType(string sqlType);

        protected abstract void Load(string tablePrefix, string connection);

        protected Generator(string excludeColumns)
        {
            this.excludeColumns = new HashSet<string>();

            foreach (var e in excludeColumns.Split(','))
                this.excludeColumns.Add(e);
        }

        public string Run(string _namespace, string tablePrefix = "", string stampColumn = "__Stamp", string connection = null)
        {
            this.stampColumn = stampColumn;
            Load(tablePrefix, connection);
            return BuildCode(_namespace).ToString();
        }

        CodeTextBuilder BuildCode(string _namespace)
        {
            var
                builder = new CodeTextBuilder();

            builder.AppendLine("using System;");
            builder.AppendLine("using System.Data.Common;");
            builder.AppendLine("using PKTools.Database;");

            builder.AppendLine("namespace " + _namespace);
            builder.OpenScope();

            foreach (var table in this.tables.Where(e => e.IsTable))
            {
                var
                    tableCode = FrendlyName(table.Code);

                builder.AppendLine("public partial class {0} : {1}", tableCode, table.IsStamped ? "SqlStampedObject" : "SqlObject");
                builder.OpenScope();

                builder.AppendLine("public const string SelectQuery = \"{0}{1} FROM {2}\";",
                    string.Join(",", table.Columns.Select(e => this.sql.EscapeName(e.Code))),
                    table.IsStamped ? "," + this.sql.EscapeName(this.stampColumn) : String.Empty,
                    this.sql.EscapeName(table.Code));

                foreach (var column in table.Columns)
                {
                    builder.AppendLine("{0} __f{1};", column.GetCSharpType(), column.ID);

                    builder.AppendLine("public {0} {1}", column.GetCSharpType(), column.Code == tableCode ? '_' + column.Code : column.Code);
                    builder.OpenScope();

                    builder.AppendLine("get {{ return this.__f{0}; }}", column.ID);
                    builder.AppendLine("set");
                    builder.OpenScope();

                    builder.AppendLine("if (this.__f{0} == value) return;", column.ID);
                    builder.AppendLine("this.__f{0} = value; this.__changed[{0}] = true;", column.ID);

                    builder.CloseScope();

                    builder.CloseScope();
                }

                int
                    identity = table.Columns.FindIndex(e => e.IsIdentity);

                builder.AppendLine("public {0}()", tableCode);
                builder.OpenScope();

                builder.AppendLine("this.__table = \"{0}\";", table.Code);
                builder.AppendLine("this.__fields = new [] {{ {0} }};", string.Join(",", table.Columns.Select(e => '"' + e.Code + '"')));
                builder.AppendLine("this.__map = new Func<object>[] {{ {0} }};", string.Join(",", table.Columns.Select((e, j) => "()=>this.__f" + j)));
                builder.AppendLine("this.__changed = new bool[{0}];", table.Columns.Count);
                builder.AppendLine("this.__isPK = new [] {{ {0} }};", string.Join(",", table.Columns.Select(e => e.IsPK ? "true" : "false")));
                builder.AppendLine("this.__identity = {0};", identity);

                builder.CloseScope();

                if (identity != -1)
                {
                    builder.AppendLine("protected override void SetIdentity(int value)");
                    builder.OpenScope();
                    builder.AppendLine("this.__f{0} = value;", identity);
                    builder.CloseScope();
                }

                if (table.HasPK())
                {
                    builder.AppendLine("public static {0} LoadByPK(Sql sql, {1})",
                        tableCode,
                        string.Join(",", table.Columns.Where(e => e.IsPK).Select(e => e.GetCSharpType() + ' ' + e.Code)));

                    builder.OpenScope();
                    builder.AppendLine("sql.Clear();");

                    foreach (var column in table.Columns.Where(e => e.IsPK))
                        builder.AppendLine("sql.AddInput(\"{0}\", {0});", column.Code);

                    builder.AppendLine("return LoadRecord<{0}>(sql, $\"SELECT {{SelectQuery}} WHERE {1}\");",
                        tableCode,
                        string.Join(" AND ", table.Columns.Where(e => e.IsPK).Select(e => this.sql.EscapeName(e.Code) + "=@" + e.Code)));

                    builder.CloseScope();
                }

                builder.AppendLine("protected override void LoadData(DbDataReader reader)");
                builder.OpenScope();

                foreach (var column in table.Columns)
                    if (column.IsNull)
                    {
                        if (column.CSharpType == typeof(string))
                            builder.AppendLine("this.__f{0} = EmptyToNull(reader[\"{1}\"]);", column.ID, column.Code);
                        else
                            builder.AppendLine("this.__f{0} = {1}<{2}>(reader[\"{3}\"]);", column.ID, column.CSharpType.IsValueType ? "ToNullable" : "ToObject", column.CSharpType.Name, column.Code);
                    }
                    else
                        builder.AppendLine("this.__f{0} = ({1})reader[\"{2}\"];", column.ID, column.CSharpType.Name, column.Code);

                builder.CloseScope();

                if (table.IsStamped)
                    builder.AppendLine("protected override string __StampCode {{ get {{ return \"{0}\"; }} }}", this.stampColumn);

                builder.CloseScope();
            }

            builder.CloseScope();

            return builder;
        }

        static string FrendlyName(string value)
        {
            var
                elements = value.Split('_').Where(e => e != string.Empty).ToArray();

            if (elements.Length == 1)
                return value;

            return string.Concat(elements.Select(e =>
            {
                var
                    temp = e.ToLower();

                return char.ToUpper(temp[0]) + temp.Remove(0, 1);
            }));
        }
    }
}
